// process.cpp
// Copyright (C) 2008 Sam Bateman
// samuel.bateman@gmail.com
//
// This file is part of DVD On Fire.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

//! @file process.cpp
//! @author Sam Bateman
//! @brief Source file for the Process class.

#include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include "lock.h"
#include "process.h"
#include "thread.h"
#include "util.h"
using namespace std;

namespace dvdonfire
{
    namespace lib
    {
        // struct Process::Impl {{{
        struct Process::Impl
        {
            Lock<int> *exit_status;
            int        fd_err;
            int        fd_out;
            pid_t      pid;
            Thread    *thread;
        };
        // }}}
        // Process::Process(args) {{{
        //! @brief Constructor.
        //! @param args Command-line arguments to the process.
        //! @param num_args Length of the args array.
        Process::Process(const std::string *args, size_t num_args)
        {
            // create output pipes and fork a new process
            impl = new Impl;
            int pipe_err[2];
            int pipe_out[2];
            pipe(pipe_err);
            pipe(pipe_out);
            impl->pid = fork();

            // Child {{{
            if (impl->pid == 0){
                // create an array suitable for exec()
                char **real_args = new char*[num_args + 1];
                for (size_t i = 0; i < num_args; ++i){
                    real_args[i] = new char[args[i].size() + 1];
                    sprintf(real_args[i], "%s", args[i].c_str());
                }
                real_args[num_args] = NULL;

                // setup outputs and call exec()
                dup2(pipe_out[1], STDOUT_FILENO);
                dup2(pipe_err[1], STDERR_FILENO);
                execvp(real_args[0], real_args);

                // if we've gotten here, there's a problem
                string pretty_args = "\"" + args[0] + "\"";
                for (size_t i = 1; i < num_args; ++i){
                    pretty_args += ", \"" + args[i] + "\"";
                }
                throw runtime_error("Process::Process([" + pretty_args
                                  + "], " + numToStr(num_args)
                                  + "): execvp() failed, errno = "
                                  + numToStr(errno) + ", strerrno = "
                                  + strerror(errno));
            }
            // }}}
            // Parent {{{
            else {
                // setup output and start a thread to monitor the child
                impl->fd_out = pipe_out[0];
                impl->fd_err = pipe_err[0];
                fcntl(impl->fd_out, F_SETFL, O_NONBLOCK);
                fcntl(impl->fd_err, F_SETFL, O_NONBLOCK);
                impl->exit_status = new Lock<int>(-1);
                impl->thread = new Thread(this);
                impl->thread->start();
            }
            // }}}
        }
        // }}}
        // Process::~Process() {{{
        //! @brief Destructor.
        Process::~Process()
        {
            delete impl->thread;
            if (isRunning()) kill();
            delete impl->exit_status;
            close(impl->fd_err);
            close(impl->fd_out);
            delete impl;
        }
        // }}}
        // Process::getExitStatus() {{{
        //! @brief Gets the exit status.
        //! @return Exit status of the child process.
        int Process::getExitStatus() const
        {
            impl->exit_status->lock();
            int es = impl->exit_status->get();
            impl->exit_status->unlock();
            return es;
        }
        // }}}
        // Process::isRunning() {{{
        //! @brief Returns the child's running state.
        //! @return Whether the child process is running.
        bool Process::isRunning() const
        {
            return getExitStatus() == -1;
        }
        // }}}
        // Process::kill() {{{
        //! @brief Kills the child.
        //!
        //! It's not as bad as it sounds.
        void Process::kill()
        {
            if (!isRunning()) return;
            ::kill(impl->pid, SIGTERM);
            impl->exit_status->set(-2);
        }
        // }}}
        // Process::run() {{{
        //! @brief Monitors the child process.
        //!
        //! Runs in a separate thread to continually update the status of the
        //! child process, particularly the exit status.
        void Process::run()
        {
            int status;
            while (waitpid(impl->pid, &status, 0) > 0){
                if (impl->thread->yield()) return;
                if (!WIFEXITED(status) && !WIFSIGNALED(status)) continue;
                impl->exit_status->lock();
                if (WIFSIGNALED(status)) impl->exit_status->set(-2);
                else impl->exit_status->set(WEXITSTATUS(status));
                impl->exit_status->unlock();
                return;
            }
        }
        // }}}
    }
}
